Marginal plots

Author

Andrés Devegili Andrés Devegili

Introduction

Marginal plots live on the “margins” of a main plot, adding extra information about the X and Y variables. In the example below, the marginals show how each group is distributed along each axis, complementing the relationships in the main plot.


Layouts

Marginal plots can be placed in different positions around the main plot, but they are most commonly shown on one side or two sides, most often above (x-axis) and to the right (y-axis).
They can also be layered, stacking multiple plot types on the same margin for richer context.

The main plot should always be the star

Marginal plots are there to provide complementary information, not to steal the spotlight.


Inventory

Marginal plots come in many forms. Histograms, boxplots, density curves, strip plots, and violins are common choices, though almost any plot type can be used. When you add more than one marginal, they can either match (same type) or be mixed (different types) to enrich the story. You can also layer them to add even more context.

More isn’t always better

Adding too many marginals can clutter your plot and overwhelm the reader.


Implementation

Some recipes in Python and R.

Python: Two-sides Histogram

(Key function: seaborn.jointplot)

Code
import seaborn as sns
import matplotlib.pyplot as plt

# Dataset
tips = sns.load_dataset("tips")

# MAIN + MARGINAL PLOTS
g = sns.jointplot(
    data=tips, x="total_bill", y="tip",
    kind="scatter", height=6, marginal_ticks=True,
    joint_kws={"s": 80, "alpha": 0.8}
)
sns.set_theme(style="white")

plt.subplots_adjust(top=0.9)

_ = g.set_axis_labels("Total bill (USD)", "Tip (USD)", labelpad=10)

# Optional: clean marginals
for ax in (g.ax_marg_x, g.ax_marg_y):
    _ = sns.despine(ax=ax, top=True, right=True, left=True, bottom=True)
    ax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)

plt.show()

Python: Two-sides layered (boxplot + histogram)

(Key function: plt.subplot_mosaic)

Code
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd

# Data
df = pd.read_csv("https://raw.githubusercontent.com/selva86/datasets/master/mtcars.csv")
df["cyl"] = df["cyl"].astype(str)


palette = sns.color_palette("Purples", n_colors=df["cyl"].nunique())
hist_color = "#9b59b6"   # medium purple for histograms

# Layout: KEY to arrange the Marginal plot LAYERS
scheme = """
BBB..
DDD..
AAAEC
AAAEC
AAAEC
"""

fig, axs = plt.subplot_mosaic(scheme, figsize=(8, 6), constrained_layout=False)
axA, axB, axC, axD, axE = axs["A"], axs["B"], axs["C"], axs["D"], axs["E"]

# MAIN (A): scatter
sns.scatterplot(
    data=df, x="mpg", y="wt", hue="cyl", palette=palette,
    s=70, alpha=0.85, legend=False, ax=axA
)
axA.set_xlabel("Miles per gallon (mpg)")
axA.set_ylabel("Weight (1000 lbs)")

# TOP (B): histogram of x (mpg)
axB.hist(df["mpg"], bins=15, color=hist_color, alpha=0.7, edgecolor="white")
axB.set_xlim(axA.get_xlim())
for sp in axB.spines.values(): sp.set_visible(False)
axB.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)

# TOP (D): boxplots of mpg by cyl
sns.boxplot(
    data=df, x="mpg", y="cyl", orient="h",
    palette=palette, fliersize=2, linewidth=1,width=0.8, ax=axD
)
axD.set_xlim(axA.get_xlim())
axD.set_xlabel("") ; axD.set_ylabel("")
for sp in axD.spines.values(): sp.set_visible(False)
axD.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)

# RIGHT (E): boxplots of wt by cyl
sns.boxplot(
    data=df, x="cyl", y="wt",
    palette=palette, fliersize=2, linewidth=1, width=0.8, ax=axE
)
axE.set_ylim(axA.get_ylim())
axE.set_xlabel("") ; axE.set_ylabel("")
for sp in axE.spines.values(): sp.set_visible(False)
axE.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)

# RIGHT (C): histogram of y (wt)
axC.hist(df["wt"], bins=12, orientation="horizontal",
         color=hist_color, alpha=0.7, edgecolor="white")
axC.set_ylim(axA.get_ylim())
for sp in axC.spines.values(): sp.set_visible(False)
axC.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False)

plt.show();

R: Two-sides dentity curves

(Key function: ggExtra::ggMarginal)

Code
library(ggplot2)
library(ggExtra)

# Main ggplot scatter
p2 <- ggplot(mtcars, aes(x = mpg, y = wt)) +
  geom_point(color = "darkgreen", size = 4, alpha = 0.6)+
  theme_minimal()+
  labs(
    x = "Miles per gallon (mpg)",
    y = "Weight (1000 lbs)"
  )

# Add MARGINAL PLOTS
ggMarginal(p2, type = "density", fill = "lightgreen", color = "darkgreen", alpha = 0.5)

R: Two-sides hybrid (density + histogram)

(Key function: gridExtra::grid.arrange)

Code
library(ggplot2)
library(gridExtra)
library(grid)

# Main scatter
p_main <- ggplot(mtcars, aes(x = mpg, y = wt)) +
  geom_point(color = "darkred", size = 6, alpha = 0.6) +
  theme_minimal() +
  labs(x = "Miles per gallon (mpg)",
       y = "Weight (1000 lbs)")

# Top marginal: density
p_top <- ggplot(mtcars, aes(x = mpg)) +
  geom_density(fill = "salmon", color = "darkred", alpha = 0.5) +
  theme_void()

# Right marginal: histogram
p_right <- ggplot(mtcars, aes(x = wt)) +
  geom_histogram(fill = "salmon", color = "darkred", bins = 12) +
  coord_flip() +
  theme_void()

# Layout
layout <- grid.arrange(
  arrangeGrob(p_top, p_main, ncol = 1, heights = c(1, 6)),
  arrangeGrob(NULL, p_right, ncol = 1, heights = c(1, 6)),
  ncol = 2, widths = c(3, 1)
)


Packages

Python Seaborn | Matplotlib

R ggplot2 | ggExtra | gridExtra


2025 · GitHub · AndresDeve